use crate::tutorials::bevy::{components::my_cpnt, events::my_ev};
use crate::tutorials::bevy::events::my_ev::*;
use crate::tutorials::bevy::res::my_res::*;
use bevy::{
    color::palettes::{
        basic::WHITE,
        css::{GREEN, RED},
    },
    core_pipeline::{
        bloom::{Bloom, BloomCompositeMode},
        tonemapping::{DebandDither, Tonemapping},
    },
    ecs::entity_disabling::Disabled,
    math::ops::powf,
    platform::collections::{HashMap, HashSet},
    prelude::*,
    render::camera::{ScalingMode, SubCameraView, Viewport},
    sprite::Wireframe2dConfig,
    render::{
        mesh::MeshTag,
        render_resource::{AsBindGroup, ShaderRef},
    },
};
use std::time::Duration;
use rand::{seq::IteratorRandom, Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;



// Animate instance
pub fn update_instanced(time: Res<Time>, mut transforms: Query<(&mut Transform, &MeshTag)>) {
    for (mut transform, index) in transforms.iter_mut() {
        // Animate the z position based on time using the index to create a spiral
        transform.translation.z = ops::sin(time.elapsed_secs() + index.0 as f32 * 0.01);
    }
}

// 
pub fn move_camera_view(
    mut movable_camera_query: Query<&mut Camera, With<my_cpnt::MovingCameraMarker>>,
    time: Res<Time>,
) {
    for mut camera in movable_camera_query.iter_mut() {
        if let Some(sub_view) = &mut camera.sub_camera_view {
            sub_view.offset.x = (time.elapsed_secs() * 150.) % 450.0 - 50.0;
            sub_view.offset.y = sub_view.offset.x;
        }
    }
}

// To ensure viewports remain the same at any window size
pub fn resize_viewports(
    window: Single<&Window, With<bevy::window::PrimaryWindow>>,
    mut viewports: Query<(&mut Camera, &my_cpnt::ExampleViewports)>,
) {
    let window_size = window.physical_size();

    let small_height = window_size.y / 5;
    let small_width = window_size.x / 8;

    let large_height = small_height * 4;
    let large_width = small_width * 4;

    let large_size = UVec2::new(large_width, large_height);

    // Enforce the aspect ratio of the small viewports to ensure the images
    // appear unstretched
    let small_dim = small_height.min(small_width);
    let small_size = UVec2::new(small_dim, small_dim);

    let small_wide_size = UVec2::new(small_dim * 2, small_dim);

    for (mut camera, example_viewport) in viewports.iter_mut() {
        if camera.viewport.is_none() {
            camera.viewport = Some(Viewport::default());
        };

        let Some(viewport) = &mut camera.viewport else {
            continue;
        };

        let (size, position) = match example_viewport {
            my_cpnt::ExampleViewports::PerspectiveMain => (large_size, UVec2::new(0, small_height)),
            my_cpnt::ExampleViewports::PerspectiveStretched => (small_size, UVec2::ZERO),
            my_cpnt::ExampleViewports::PerspectiveMoving => (small_size, UVec2::new(small_width, 0)),
            my_cpnt::ExampleViewports::PerspectiveControl => {
                (small_wide_size, UVec2::new(small_width * 2, 0))
            }
            my_cpnt::ExampleViewports::OrthographicMain => {
                (large_size, UVec2::new(large_width, small_height))
            }
            my_cpnt::ExampleViewports::OrthographicStretched => (small_size, UVec2::new(small_width * 4, 0)),
            my_cpnt::ExampleViewports::OrthographicMoving => (small_size, UVec2::new(small_width * 5, 0)),
            my_cpnt::ExampleViewports::OrthographicControl => {
                (small_wide_size, UVec2::new(small_width * 6, 0))
            }
        };

        viewport.physical_size = size;
        viewport.physical_position = position;
    }
}

// 
pub fn greet_people(
    time: Res<Time>,
    mut timer: ResMut<GreetTimer>,
    query: Query<&my_cpnt::Name, With<my_cpnt::Person>>,
) {
    if timer.0.tick(time.delta()).just_finished() {
        for name in &query {
            println!("hello {}!", name.0);
        }
    }
}

pub fn update_people(mut query: Query<&mut my_cpnt::Name, With<my_cpnt::Person>>) {
    for mut name in &mut query {
        if name.0 == "Elaina Proctor" {
            name.0 = "Elaina Hume".to_string();
            break; // We don't need to change any other names.
        }
    }
}

//
pub fn toggle_wireframe(
    mut wireframe_config: ResMut<Wireframe2dConfig>,
    keyboard: Res<ButtonInput<KeyCode>>,
) {
    if keyboard.just_pressed(KeyCode::Space) {
        wireframe_config.global = !wireframe_config.global;
    }
}

//
pub fn draw_cursor_2d_viewport(
    camera_query: Single<(&Camera, &GlobalTransform)>,
    window: Query<&Window>,
    mut gizmos: Gizmos,
) {
    let (camera, camera_transform) = *camera_query;
    let Ok(window) = window.single() else {
        return;
    };

    let Some(cursor_position) = window.cursor_position() else {
        return;
    };

    // Calculate a world position based on the cursor's position.
    let Ok(world_pos) = camera.viewport_to_world_2d(camera_transform, cursor_position) else {
        return;
    };

    // To test Camera::world_to_viewport, convert result back to viewport space and then back to world space.
    let Ok(viewport_check) = camera.world_to_viewport(camera_transform, world_pos.extend(0.0))
    else {
        return;
    };
    let Ok(world_check) = camera.viewport_to_world_2d(camera_transform, viewport_check.xy()) else {
        return;
    };

    gizmos.circle_2d(world_pos, 10., WHITE);
    // Should be the same as world_pos
    gizmos.circle_2d(world_check, 8., RED);
}

pub fn controls_2d_viewport(
    mut camera_query: Query<(&mut Camera, &mut Transform, &mut Projection)>,
    window: Query<&Window>,
    input: Res<ButtonInput<KeyCode>>,
    time: Res<Time<Fixed>>,
) {
    let Ok(window) = window.single() else {
        return;
    };
    let Ok((mut camera, mut transform, mut projection)) = camera_query.single_mut() else {
        return;
    };
    let fspeed = 600.0 * time.delta_secs();
    let uspeed = fspeed as u32;
    let window_size = window.resolution.physical_size();

    // Camera movement controls
    if input.pressed(KeyCode::ArrowUp) {
        transform.translation.y += fspeed;
    }
    if input.pressed(KeyCode::ArrowDown) {
        transform.translation.y -= fspeed;
    }
    if input.pressed(KeyCode::ArrowLeft) {
        transform.translation.x -= fspeed;
    }
    if input.pressed(KeyCode::ArrowRight) {
        transform.translation.x += fspeed;
    }

    // Camera zoom controls
    if let Projection::Orthographic(projection2d) = &mut *projection {
        if input.pressed(KeyCode::Comma) {
            projection2d.scale *= powf(4.0f32, time.delta_secs());
        }

        if input.pressed(KeyCode::Period) {
            projection2d.scale *= powf(0.25f32, time.delta_secs());
        }
    }

    if let Some(viewport) = camera.viewport.as_mut() {
        // Viewport movement controls
        if input.pressed(KeyCode::KeyW) {
            viewport.physical_position.y = viewport.physical_position.y.saturating_sub(uspeed);
        }
        if input.pressed(KeyCode::KeyS) {
            viewport.physical_position.y += uspeed;
        }
        if input.pressed(KeyCode::KeyA) {
            viewport.physical_position.x = viewport.physical_position.x.saturating_sub(uspeed);
        }
        if input.pressed(KeyCode::KeyD) {
            viewport.physical_position.x += uspeed;
        }

        // Bound viewport position so it doesn't go off-screen
        viewport.physical_position = viewport
            .physical_position
            .min(window_size - viewport.physical_size);

        // Viewport size controls
        if input.pressed(KeyCode::KeyI) {
            viewport.physical_size.y = viewport.physical_size.y.saturating_sub(uspeed);
        }
        if input.pressed(KeyCode::KeyK) {
            viewport.physical_size.y += uspeed;
        }
        if input.pressed(KeyCode::KeyJ) {
            viewport.physical_size.x = viewport.physical_size.x.saturating_sub(uspeed);
        }
        if input.pressed(KeyCode::KeyL) {
            viewport.physical_size.x += uspeed;
        }

        // Bound viewport size so it doesn't go off-screen
        viewport.physical_size = viewport
            .physical_size
            .min(window_size - viewport.physical_position)
            .max(UVec2::new(20, 20));
    }
}

//
pub fn update_bloom_settings(
    camera: Single<(Entity, &Tonemapping, Option<&mut Bloom>), With<Camera>>,
    mut text: Single<&mut Text>,
    mut commands: Commands,
    keycode: Res<ButtonInput<KeyCode>>,
    time: Res<Time>,
) {
    let (camera_entity, tonemapping, bloom) = camera.into_inner();

    match bloom {
        Some(mut bloom) => {
            text.0 = "Bloom (Toggle: Space)\n".to_string();
            text.push_str(&format!("(Q/A) Intensity: {}\n", bloom.intensity));
            text.push_str(&format!(
                "(W/S) Low-frequency boost: {}\n",
                bloom.low_frequency_boost
            ));
            text.push_str(&format!(
                "(E/D) Low-frequency boost curvature: {}\n",
                bloom.low_frequency_boost_curvature
            ));
            text.push_str(&format!(
                "(R/F) High-pass frequency: {}\n",
                bloom.high_pass_frequency
            ));
            text.push_str(&format!(
                "(T/G) Mode: {}\n",
                match bloom.composite_mode {
                    BloomCompositeMode::EnergyConserving => "Energy-conserving",
                    BloomCompositeMode::Additive => "Additive",
                }
            ));
            text.push_str(&format!("(Y/H) Threshold: {}\n", bloom.prefilter.threshold));
            text.push_str(&format!(
                "(U/J) Threshold softness: {}\n",
                bloom.prefilter.threshold_softness
            ));
            text.push_str(&format!("(I/K) Horizontal Scale: {}\n", bloom.scale.x));
            // control
            if keycode.just_pressed(KeyCode::Space) {
                commands.entity(camera_entity).remove::<Bloom>();
            }

            let dt = time.delta_secs();

            if keycode.pressed(KeyCode::KeyA) {
                bloom.intensity -= dt / 10.0;
            }
            if keycode.pressed(KeyCode::KeyQ) {
                bloom.intensity += dt / 10.0;
            }
            bloom.intensity = bloom.intensity.clamp(0.0, 1.0);

            if keycode.pressed(KeyCode::KeyS) {
                bloom.low_frequency_boost -= dt / 10.0;
            }
            if keycode.pressed(KeyCode::KeyW) {
                bloom.low_frequency_boost += dt / 10.0;
            }
            bloom.low_frequency_boost = bloom.low_frequency_boost.clamp(0.0, 1.0);

            if keycode.pressed(KeyCode::KeyD) {
                bloom.low_frequency_boost_curvature -= dt / 10.0;
            }
            if keycode.pressed(KeyCode::KeyE) {
                bloom.low_frequency_boost_curvature += dt / 10.0;
            }
            bloom.low_frequency_boost_curvature =
                bloom.low_frequency_boost_curvature.clamp(0.0, 1.0);

            if keycode.pressed(KeyCode::KeyF) {
                bloom.high_pass_frequency -= dt / 10.0;
            }
            if keycode.pressed(KeyCode::KeyR) {
                bloom.high_pass_frequency += dt / 10.0;
            }
            bloom.high_pass_frequency = bloom.high_pass_frequency.clamp(0.0, 1.0);

            if keycode.pressed(KeyCode::KeyG) {
                bloom.composite_mode = BloomCompositeMode::Additive;
            }
            if keycode.pressed(KeyCode::KeyT) {
                bloom.composite_mode = BloomCompositeMode::EnergyConserving;
            }

            if keycode.pressed(KeyCode::KeyH) {
                bloom.prefilter.threshold -= dt;
            }
            if keycode.pressed(KeyCode::KeyY) {
                bloom.prefilter.threshold += dt;
            }
            bloom.prefilter.threshold = bloom.prefilter.threshold.max(0.0);

            if keycode.pressed(KeyCode::KeyJ) {
                bloom.prefilter.threshold_softness -= dt / 10.0;
            }
            if keycode.pressed(KeyCode::KeyU) {
                bloom.prefilter.threshold_softness += dt / 10.0;
            }
            bloom.prefilter.threshold_softness = bloom.prefilter.threshold_softness.clamp(0.0, 1.0);

            if keycode.pressed(KeyCode::KeyK) {
                bloom.scale.x -= dt * 2.0;
            }
            if keycode.pressed(KeyCode::KeyI) {
                bloom.scale.x += dt * 2.0;
            }
            bloom.scale.x = bloom.scale.x.clamp(0.0, 16.0);
        }

        None => {
            text.0 = "Bloom: Off (Toggle: Space)\n".to_string();

            if keycode.just_pressed(KeyCode::Space) {
                commands.entity(camera_entity).insert(Bloom::default());
            }
        }
    }

    text.push_str(&format!("(O) Tonemapping: {:?}\n", tonemapping));
    if keycode.just_pressed(KeyCode::KeyO) {
        commands
            .entity(camera_entity)
            .insert(next_tonemap(tonemapping));
    }
}

/// Get the next Tonemapping algorithm
fn next_tonemap(tonemapping: &Tonemapping) -> Tonemapping {
    match tonemapping {
        Tonemapping::None => Tonemapping::AcesFitted,
        Tonemapping::AcesFitted => Tonemapping::AgX,
        Tonemapping::AgX => Tonemapping::BlenderFilmic,
        Tonemapping::BlenderFilmic => Tonemapping::Reinhard,
        Tonemapping::Reinhard => Tonemapping::ReinhardLuminance,
        Tonemapping::ReinhardLuminance => Tonemapping::SomewhatBoringDisplayTransform,
        Tonemapping::SomewhatBoringDisplayTransform => Tonemapping::TonyMcMapface,
        Tonemapping::TonyMcMapface => Tonemapping::None,
    }
}

//
pub fn disable_entities_on_click(
    trigger: Trigger<Pointer<Click>>,
    valid_query: Query<&my_cpnt::DisableOnClick>,
    mut commands: Commands,
) {
    let clicked_entity = trigger.target();
    // Windows and text are entities and can be clicked!
    // We definitely don't want to disable the window itself,
    // because that would cause the app to close!
    if valid_query.contains(clicked_entity) {
        // Just add the `Disabled` component to the entity to disable it.
        // Note that the `Disabled` component is *only* added to the entity,
        // its children are not affected.
        commands.entity(clicked_entity).insert(Disabled);
    }
}

// The query here will not find entities with the `Disabled` component,
// because it does not explicitly include it.
pub fn list_all_named_entities(
    query: Query<&Name>,
    mut name_text_query: Query<&mut Text, With<my_cpnt::EntityNameText>>,
    mut commands: Commands,
) {
    let mut text_string = String::from("Named entities found:\n");
    // Query iteration order is not guaranteed, so we sort the names
    // to ensure the output is consistent.
    for name in query.iter().sort::<&Name>() {
        text_string.push_str(&format!("{:?}\n", name));
    }

    if let Ok(mut text) = name_text_query.single_mut() {
        *text = Text::new(text_string);
    } else {
        commands.spawn((
            my_cpnt::EntityNameText,
            Text::default(),
            Node {
                position_type: PositionType::Absolute,
                top: Val::Px(12.0),
                right: Val::Px(12.0),
                ..default()
            },
        ));
    }
}

pub fn reenable_entities_on_space(
    mut commands: Commands,
    // This query can find disabled entities,
    // because it explicitly includes the `Disabled` component.
    disabled_entities: Query<Entity, With<Disabled>>,
    input: Res<ButtonInput<KeyCode>>,
) {
    if input.just_pressed(KeyCode::Space) {
        for entity in disabled_entities.iter() {
            // To re-enable an entity, just remove the `Disabled` component.
            commands.entity(entity).remove::<Disabled>();
        }
    }
}

//
pub fn explode_mine(
    trigger: Trigger<Explode>,
    query: Query<&my_cpnt::Mine>,
    mut commands: Commands,
) {
    // If a triggered event is targeting a specific entity you can access it with `.target()`
    let id = trigger.target();
    let Ok(mut entity) = commands.get_entity(id) else {
        return;
    };
    info!("Boom! {} exploded.", id.index());
    entity.despawn();
    let mine = query.get(id).unwrap();
    // Trigger another explosion cascade.
    commands.trigger(ExplodeMines {
        pos: mine.pos,
        radius: mine.size,
    });
}

pub fn explode_mines_cascade(
    trigger: Trigger<ExplodeMines>,
    mines: Query<&my_cpnt::Mine>,
    index: Res<SpatialIndex>,
    mut commands: Commands,
) {
    // You can access the trigger data via the `Observer`
    let event = trigger.event();
    // Access resources
    for e in index.get_nearby(event.pos) {
        // Run queries
        let mine = mines.get(e).unwrap();
        if mine.pos.distance(event.pos) < mine.size + event.radius {
            // And queue commands, including triggering additional events
            // Here we trigger the `Explode` event for entity `e`
            commands.trigger_targets(Explode, e);
        }
    }
}

pub fn on_add_mine(
    trigger: Trigger<OnAdd, my_cpnt::Mine>,
    query: Query<&my_cpnt::Mine>,
    mut index: ResMut<SpatialIndex>,
) {
    let mine = query.get(trigger.target()).unwrap();
    let tile = (
        (mine.pos.x / CELL_SIZE).floor() as i32,
        (mine.pos.y / CELL_SIZE).floor() as i32,
    );
    index.map.entry(tile).or_default().insert(trigger.target());
}

// Remove despawned mines from our index
pub fn on_remove_mine(
    trigger: Trigger<OnRemove, my_cpnt::Mine>,
    query: Query<&my_cpnt::Mine>,
    mut index: ResMut<SpatialIndex>,
) {
    let mine = query.get(trigger.target()).unwrap();
    let tile = (
        (mine.pos.x / CELL_SIZE).floor() as i32,
        (mine.pos.y / CELL_SIZE).floor() as i32,
    );
    index.map.entry(tile).and_modify(|set| {
        set.remove(&trigger.target());
    });
}

// Draw a circle for each mine using `Gizmos`
pub fn draw_shapes_mine(mut gizmos: Gizmos, mines: Query<&my_cpnt::Mine>) {
    for mine in &mines {
        gizmos.circle_2d(
            mine.pos,
            mine.size,
            Color::hsl((mine.size - 4.0) / 16.0 * 360.0, 1.0, 0.8),
        );
    }
}

// Trigger `ExplodeMines` at the position of a given click
pub fn handle_click_mine(
    mouse_button_input: Res<ButtonInput<MouseButton>>,
    camera: Single<(&Camera, &GlobalTransform)>,
    windows: Query<&Window>,
    mut commands: Commands,
) {
    let Ok(windows) = windows.single() else {
        return;
    };

    let (camera, camera_transform) = *camera;
    if let Some(pos) = windows
        .cursor_position()
        .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok())
        .map(|ray| ray.origin.truncate())
    {
        if mouse_button_input.just_pressed(MouseButton::Left) {
            commands.trigger(ExplodeMines { pos, radius: 1.0 });
        }
    }
}

// 
/// A normal bevy system that attacks a piece of the goblin's armor on a timer.
pub fn attack_armor(entities: Query<Entity, With<my_cpnt::Armor>>, mut commands: Commands) {
    let mut rng = rand::rng();
    if let Some(target) = entities.iter().choose(&mut rng) {
        let damage = rng.random_range(1..20);
        commands.trigger_targets(Attack { damage }, target);
        info!("⚔️  Attack for {} damage", damage);
    }
}

pub fn attack_hits(trigger: Trigger<my_ev::Attack>, name: Query<&Name>) {
    if let Ok(name) = name.get(trigger.target()) {
        info!("Attack hit {}", name);
    }
}

/// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage.
pub fn block_attack(mut trigger: Trigger<my_ev::Attack>, armor: Query<(&my_cpnt::Armor, &Name)>) {
    let (armor, name) = armor.get(trigger.target()).unwrap();
    let attack = trigger.event_mut();
    let damage = attack.damage.saturating_sub(**armor);
    if damage > 0 {
        info!("🩸 {} damage passed through {}", damage, name);
        // The attack isn't stopped by the armor. We reduce the damage of the attack, and allow
        // it to continue on to the goblin.
        attack.damage = damage;
    } else {
        info!("🛡️  {} damage blocked by {}", attack.damage, name);
        // Armor stopped the attack, the event stops here.
        trigger.propagate(false);
        info!("(propagation halted early)\n");
    }
}

/// A callback on the armor wearer, triggered when a piece of armor is not able to block an attack,
/// or the wearer is attacked directly.
pub fn take_damage(
    trigger: Trigger<my_ev::Attack>,
    mut hp: Query<(&mut my_cpnt::HitPoints, &Name)>,
    mut commands: Commands,
    mut app_exit: EventWriter<AppExit>,
) {
    let attack = trigger.event();
    let (mut hp, name) = hp.get_mut(trigger.target()).unwrap();
    **hp = hp.saturating_sub(attack.damage);

    if **hp > 0 {
        info!("{} has {:.1} HP", name, hp.0);
    } else {
        warn!("💀 {} has died a gruesome death", name);
        commands.entity(trigger.target()).despawn();
        app_exit.write(AppExit::Success);
    }

    info!("(propagation reached root)\n");
}

// 
pub fn interact_bodies(mut query: Query<(&my_cpnt::Mass, &GlobalTransform, &mut my_cpnt::Acceleration)>) {
    let mut iter = query.iter_combinations_mut();
    while let Some([
        (my_cpnt::Mass(m1), transform1, mut acc1), 
        (my_cpnt::Mass(m2), transform2, mut acc2)
        ]) =
        iter.fetch_next()
    {
        let delta = transform2.translation() - transform1.translation();
        let distance_sq: f32 = delta.length_squared();

        let f = my_cpnt::GRAVITY_CONSTANT / distance_sq;
        let force_unit_mass = delta * f;
        acc1.0 += force_unit_mass * *m2;
        acc2.0 -= force_unit_mass * *m1;
    }
}

pub fn integrate(time: Res<Time>, mut query: Query<(&mut my_cpnt::Acceleration, &mut Transform, &mut my_cpnt::LastPos)>) {
    let dt_sq = time.delta_secs() * time.delta_secs();
    for (mut acceleration, mut transform, mut last_pos) in &mut query {
        // verlet integration
        // x(t+dt) = 2x(t) - x(t-dt) + a(t)dt^2 + O(dt^4)

        let new_pos = transform.translation * 2.0 - last_pos.0 + acceleration.0 * dt_sq;
        acceleration.0 = Vec3::ZERO;
        last_pos.0 = transform.translation;
        transform.translation = new_pos;
    }
}

pub fn look_at_star(
    mut camera: Single<&mut Transform, (With<Camera>, Without<my_cpnt::Star>)>,
    star: Single<&Transform, With<my_cpnt::Star>>,
) {
    let new_rotation = camera
        .looking_at(star.translation, Vec3::Y)
        .rotation
        .lerp(camera.rotation, 0.1);
    camera.rotation = new_rotation;
}